<?php
/**
 * Created by PhpStorm.
 * @author inhere
 * @date   2015-08-05
 * Router.php
 */
namespace ulue\core\router;


use Ioc,Ulue,Debug;
use ulue\core\abstracts\AbstractCore;
use ulue\core\module\AbstractModule as Module;
use ulue\libs\front\Html;
use ulue\libs\traits\TraitGetOption;
use ulue\libs\helpers\ArrayHelper;
use ulue\libs\helpers\DataHelper;
use ulue\core\request\Request;

class Router extends AbstractCore
{
    /**
     * url格式:
     * 1 path                          ...xx.com/index.php/port/edit/id/34
     * 2 rewrite (需开启对应 Apache/nginx 模块,对应配置) ...xx.com/port/edit/id/34
     * 3 compat 兼容格式                           ...xx.com/index.php?r=port/edit&id=34
     * 4 raw 原生方式                        ...xx.com/index.php?c=port&a=edit&id=34
     */
    const FORMAT_PATH    = 'path';
    const FORMAT_REWRITE = 'rewrite';
    const FORMAT_COMPAT  = 'compat';
    const FORMAT_RAW     = 'raw';

    /**
     * 设置路由字符串访问格式，会影响到 Uri::create()
     * @var string
     */
    public $format = 'rewrite';

    public $separator = '/';

    /**
     * url 访问允许的扩展名/后缀名，已有点号
     * @var string url suffix .html
     */
    protected $suffix = '.html';

    /**
     * 兼容格式路由变量名
     * @var string
     */
    public $routeVar    = 'r';

    /**
     * @var bool 设置是否启用路由规则匹配
     */
    protected $enabledRule = false;

    public $rules = [];

    public $ruleKeywords;


    /**
     * @var RuleParser
     */
    // protected $ruleParser;

    /**
     * 是 path 格式的路由
     * e.g. admin/user/login
     * @var boolean
     */
    private $_isRawRoute = false;

    /**
     * 禁用 原始参数方式来请求路由
     * @var boolean
     */
    protected $disabledRaw = false;

    /**
     * 原始参数方式,获取路由信息的变量名设置
     * @var array
     * e.g. index.php?m=admin&c=user&a=login
     */
    protected $vars =  [
            'module'     => 'm',  // 模块
            'controller' => 'c',  // 控制器
            'action'     => 'a'   // 动作方法
        ];

    /**
     * 请求的默认路由 path (未应用规则之前的)
     * @var string
     */
    private $_rawPath;

    /**
     * path路由  (规则匹配成功后会发生变化)
     * @var string
     */
    private $_path;


    // 解析后,url全部请求参数信息
    private $_rawRoute   = [];

    /**
     * array路由
     * 解析后,url全部请求参数信息
     * @todo  后面访问 module controller，解析 url rule，信息可能会随之变化
     * @var array
     */
    private $_route     = [
        'module'       => '',
        'controller'   => '',
        'action'       => '',
    ];

    /**
     * 路由信息之外的查询参数信息
     * @var array
     */
    protected $params      = [];

    //config:route.rules '<@ALL_MATCH>'     => '<c:index>/<a:about>',// 全部请求都定向到 '/c=index&a=about'
    const ALL_MATCH   = '@ALL_MATCH';


/////////////////////////////// 解析请求 路由规则 ///////////////////////////////

    public function resolve()
    {
        Debug::trace("i 开始处理请求,并解析 route rule");

        $req   = Ulue::$di->get('request');
        $route = '';
        // $route = $this->_route;

        // 有路由信息
        if ( $this->_fetchRouteInfo($req) ) {
            if ( $this->_isRawRoute ) {
                $route = $this->_route;

            // 解析 path路由
            } else {

                // 特殊
                if ( $this->_path === 'favicon.ico' ) {
                    if (APP_DEBUG) {
                        \Trigger::notice('站点没有favicon.ico');
                    }

                    Ulue::$app->quit();
                }

                $this->_rawPath = $this->_path;

                // 匹配成功
                if ( $this->ruleMatch($req) ) {
                    $route = $this->_route;
                } elseif ($this->_path) {

                    // 清理后缀
                    $route = $this->_path = preg_replace('#'. $this->suffix .'$#u', '', $this->_path);
                }
            }
        }

        $this->_rawRoute = $route;

        Debug::trace('初始请求处理完成',[
            '初始请求路由信息'  => $route,
            '初始请求路由path'  => $this->_path,
            '请求参数信息'      => $this->params
        ]);

        return $route;
    }


    /**
     * 解析前, url 完整匹配
     * @param  Request $req [description]
     * @return bool
     */
    protected function ruleMatch(Request $req)
    {
        if ( (bool)$this->enabledRule && $this->_path) {

            // $this->handleRules();
            // $scriptName = $req->scriptName;

            // e.g. index.php/test_article => title=test_article
            // $path = preg_replace( "/(\/)$scriptName(\/|\?)/",'', $req->uri);

            $ruleParser = new RuleParser($this->rules, $this);

            return $ruleParser->matchPath( $path );
        }

        return false;
    }

    /**
     * 获取页面请求的路由信息 (array) route | (string) path
     * 得到请求路由信息，同时处理查询参数字符串，有路由信息返回true
     *
     * 原始url格式(e.g. index.php?m=admin&c=user&a=login)将会直接使用，不必再解析。
     *
     * PATH_INFO
     * 必须有 入口文件(index.php) 和 / 分隔 才会出现
     * @example http://xxx.com/index.php/bill/index/?kk=rrt&p=6
     *    $_SERVER['PATH_INFO']    == '/bill/index/'
     *    $_SERVER['QUERY_STRING'] == 'kk=rrt&p=6'
     * 快速路由
     * URL: http://xxx.com/index/global
     *    $_SERVER['REDIRECT_URL'] = $_SERVER['QUERY_STRING'] = '/index/global'
     * 快速路由与参数形式混合时
     * URL: http://local.test.com/user/login?ar=c3&b=23
     *    $_SERVER['REDIRECT_URL'] = '/user/login'
     *    $_SERVER['QUERY_STRING'] = '/user/login&ar=c3&b=23';
     */
    protected function _fetchRouteInfo(Request $req)
    {
        $urls     = UriHelper::parseUrl($req->uri);

        // 去掉入口脚本 e.g. scriptName:dev.php path:dev.php/admin/user -> after path: admin/user
        $urls['path'] = trim( str_replace( $req->scriptUrl, '', $urls['path']),'/ ');

        /** 无路由信息 @example http://xxx.com || http://xxx.com/index.php */
        if (empty($urls['query']) && !$urls['path'] ) {
            return false;
        }

        if ( !empty($urls['query']) ) {
            parse_str($urls['query'], $querys);

            $this->params = $querys;

            // 允许以原始参数方式 设置请求路由
            if ( !$this->disabledRaw ) {
                $rawRoute     = [];

                foreach ($this->vars as $name => $var) {
                    $rawRoute[$name] = $req->get($var);
                }

                /* 1.原生格式, e.g. index.php?c=user&a=login 通过 c a 得到 需要的访问路由信息 */
                if( $rawRoute = array_filter($rawRoute) ) {
                    $this->_route      = array_merge($this->_route, $rawRoute);
                    $this->_isRawRoute = true;

                    unset( $rawRoute, $querys);

                    return true;
                }
            }

            /* 2.兼容格式,支持 index.php?r=user/login 通过 r 得到 需要的访问路由path */
            if ( !empty($querys[$this->routeVar]) ) {
                $this->_path = trim($querys[$this->routeVar],'/ ');

                return true;
            }
        }

        // 有 url path e.g. index.php/user/login -> $path = user/login
        if ( $urls['path'] ) {

            $this->_path    = trim($urls['path'], '/ ');

            return true;
        }

        return false;
    }

    /**
     * 重定向跳转
     * 'login' 'user/login' 'admin/user/login'
     * @param $url
     * @param int $time
     * @param null $msg
     * @return null
     */
    public function redirect($url, $time = 0, $msg = null)
    {
        if (!is_string($url) || !$url) {
            return false;
        }

        $url        = trim($url);
        $isRoot     = substr($url, 0,1) == '/';

        if ( strpos($url,'/') !== false && !$isRoot) {
            $url        = rtrim($url,'/'); #去掉后面多余的'/'
            $routeArr   = explode('/',$url,2);

            #第一段是模块 'admin/user' 'admin/user/login'-->'admin' 在模块列表 Router::handleModule() 中
            if ( Module::isModule($routeArr[0]) ) {
                $url    = Uri::make($url);
            #第一段不是模块，即是控制器 'user/login',添上当前模块名
            } else {
                $module = $this->get('module');
                $module = $module == \Ulue::$app->defaultModule ? '': $module.'/';
                $url    = Uri::make($module.$url);
            }

        #仅一段参数
        } elseif (!$isRoot) {
            $url        = '/'.$url;
        }

        if (!headers_sent()) {
            $time == 0 ? header("Location:" . $url) : header("refresh:{$time};url={$url}");

            if (!is_null($msg)) {
                echo('<h3>'.$msg.'</h3>');
            }
        } else {
            if (!is_null($msg)) {
                echo('<h3>'.$msg.'</h3>');
            }

            $js = "setTimeout(function(){window.location.href='$url';},$time*1000);";
            echo Html::script($js);
        }

        \Ulue::quit();

        return null;
    }

//////////////////////////////////// class property ////////////////////////////////////

    /**
     * 增加查询参数信息.
     *
     * @param array $params the params
     *
     * @return self
     */
    public function addParams(array $params)
    {
        $this->params = array_merge($this->params, $params);
        $_GET         = array_merge($_GET, $params);

        return $this;
    }

    /**
     * 增加查询参数信息.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return self
     */
    public function addParam($name, $value)
    {
        if (!isset($this->params[$name])) {
            $this->params[$name] = $_GET[$name] = $value;
        }

        return $this;
    }

    /**
     * @example
     *  /status/active/id/12  =>
     *  [
     *   'status' => 'active',
     *   'id'     => '12',
     *  ]
     * @param $data
     * @return array
     */
    public function addParamsByPath($path)
    {
        $params = DataHelper::buildQueryParams($path, $this->separator);

        return $this->addParams($params);
    }

    /**
     * 获取全部请求参数信息
     * @param bool $toString
     * false return array |
     * true return query string
     * @return array|null|string
     */
    public function getParams($toString=false)
    {
        if ($toString) {
            return !$this->params ? '' : http_build_query($this->params);
        }

        return $this->params;
    }

    /**
     * 将 $this->params 组成 查询字符串
     * e.g.
     *  [
     *   'status' => 'active',
     *   'id'     => '12',
     *  ]
     *  =>  status=active&id=12
     * @return string
     */
    public function getParamsString()
    {
        return !$this->params ? '' : http_build_query($this->params);
    }

    /**
     * 获取请求参数信息
     * @param $var string 变量名
     * @param null $default
     * @return array|null|string
     */
    public function getQueryParam($var, $default=null)
    {
        if (isset($this->params[$var])) {
            return $this->params[$var];
        }

        return $default;
    }

    /**
     * Sets the 原始参数方式,获取路由信息的变量名设置
     *
     * @param array $vars the vars
     *
     * @return self
     */
    public function setVars(array $vars)
    {
        foreach ($vars as $name => $var) {
            $this->setVar($name, $var);
        }

        return $this;
    }

    public function getVars($onlyValue=false)
    {
        return $onlyValue ? array_values($this->vars) : $this->vars;
    }

    /**
     * @param  string     $name
     * @param  mixed      $default
     * @return mixed
     */
    public function getVar($name, $default=null)
    {
        return isset($this->vars[$name]) ? $this->vars[$name] : $default;
    }

    public function setVar($name, $var)
    {
        if (isset($this->vars[$name])) {
            $this->vars[$name] = (string) $var;
        }

        return $this;
    }

    //
    public function getFormat()
    {
        return $this->format;
    }

    //
    public function setFormat($value)
    {
        if ( in_array($value, self::getFormats() ) ) {
            $this->format = $value;
        }

        return $this;
    }

    public function getEnabledRule()
    {
        return $this->enabledRule;
    }

    public function setEnabledRule($value)
    {
        $this->enabledRule = (bool)$value;

        return $this;
    }

    public function getSuffix()
    {
        return $this->suffix;
    }

    public function setSuffix($value)
    {
        if ($value) {
            $this->suffix = '.'.trim($value,'. ');
        }

        return $this;
    }

    public function getPath()
    {
        return $this->_path;
    }

    /**
     * 得到实际访问路由字符串
     * @return string
     */
    public function getAccessRoute()
    {
        return $this->_path;
    }

    /**
     * 设置全部路由信息
     * @param $route
     * @return $this
     */
    public function setRoute(array $route)
    {
        foreach ($route as $name => $value) {
            $this->setRouteValue($name, $value);
        }

        return $this;
    }

    /**
     * 获取全部路由信息
     * @param bool $toString
     * @return array
     */
    public function getRoute($toString=false)
    {
        if ($toString) {
            return $this->getRouteString();
        }

        return $this->_route;
    }

    /**
     * 通过名称设置对应的路由值
     * @param  string     $name
     * @param  string     $value
     * @return self
     */
    public function setRouteValue($name, $value)
    {
        if (isset($this->route[$name])) {
            $this->route[$name] = $value;
        }

        return $this;
    }

    /**
     * 通过名称获取对应的路由值
     * @param  string     $name
     * @return string
     */
    public function getRouteValue($name)
    {
        return isset($this->route[$name]) ? $this->route[$name] : '';
    }

    /**
     * 获取路由字符串
     * @return string
     */
    public function getRouteString()
    {
        $route = $this->_route;

        return '/'.implode('/', $route);
    }


    /**
     * @return boolean
     */
    public function getDisabledRaw()
    {
        return $this->disabledRaw;
    }

    /**
     * 禁用 原始参数方式获取路由信息.
     *
     * @param boolean $disabledRaw the disabled raw
     *
     * @return self
     */
    protected function setDisabledRaw($disabledRaw)
    {
        $this->disabledRaw = (bool)$disabledRaw;

        return $this;
    }

////////////////////////////////// Other //////////////////////////////////

    //
    static public function getFormats()
    {
        return [ self::FORMAT_RAW, self::FORMAT_PATH, self::FORMAT_COMPAT, self::FORMAT_REWRITE];
    }

    static public function isUsableFormat($format)
    {
        $format = strtolower(trim($format));

        return in_array( $format, self::getFormats() ) ? $format : false ;
    }


    public function __get($name)
    {
        if (isset($this->route[$name])) {
            return $this->route[$name];
        }

        return parent::__get($name);
    }

    public function __set($name, $value)
    {
        if (isset($this->route[$name])) {
            $this->route[$name] = $value;
        } else {
            parent::__set($name);
        }
    }
}